/*
 * Copyright (C) 2012 The Guava Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.common.io;

import static com.google.common.io.SourceSinkFactory.ByteSourceFactory;
import static com.google.common.io.SourceSinkFactory.CharSourceFactory;
import static org.junit.Assert.assertArrayEquals;

import com.google.common.base.Charsets;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.util.Map.Entry;
import java.util.Random;

import junit.framework.TestSuite;

/**
 * A generator of {@code TestSuite} instances for testing {@code ByteSource} implementations.
 * Generates tests of a all methods on a {@code ByteSource} given various inputs the source is
 * expected to contain as well as sub-suites for testing the {@code CharSource} view and {@code
 * slice()} views in the same way.
 *
 * @author Colin Decker
 */
@AndroidIncompatible // Android doesn't understand tests that lack default constructors.
public class ByteSourceTester extends SourceSinkTester<ByteSource, byte[], ByteSourceFactory>
{

    private static final ImmutableList<Method> testMethods = getTestMethods(ByteSourceTester.class);

    static TestSuite tests(String name, ByteSourceFactory factory, boolean testAsCharSource)
    {
        TestSuite suite = new TestSuite(name);
        for (Entry<String, String> entry : TEST_STRINGS.entrySet())
        {
            if (testAsCharSource)
            {
                suite.addTest(suiteForString(factory, entry.getValue(), name, entry.getKey()));
            }
            else
            {
                suite.addTest(
                        suiteForBytes(
                                factory, entry.getValue().getBytes(Charsets.UTF_8), name, entry.getKey(), true));
            }
        }
        return suite;
    }

    static TestSuite suiteForString(
            ByteSourceFactory factory, String string, String name, String desc)
    {
        TestSuite suite = suiteForBytes(factory, string.getBytes(Charsets.UTF_8), name, desc, true);
        CharSourceFactory charSourceFactory = SourceSinkFactories.asCharSourceFactory(factory);
        suite.addTest(
                CharSourceTester.suiteForString(
                        charSourceFactory, string, name + ".asCharSource[Charset]", desc));
        return suite;
    }

    static TestSuite suiteForBytes(
            ByteSourceFactory factory, byte[] bytes, String name, String desc, boolean slice)
    {
        TestSuite suite = new TestSuite(name + " [" + desc + "]");
        for (Method method : testMethods)
        {
            suite.addTest(new ByteSourceTester(factory, bytes, name, desc, method));
        }

        if (slice && bytes.length > 0)
        {
            // test a random slice() of the ByteSource
            Random random = new Random();
            byte[] expected = factory.getExpected(bytes);
            // if expected.length == 0, off has to be 0 but length doesn't matter--result will be empty
            int off = expected.length == 0 ? 0 : random.nextInt(expected.length);
            int len = expected.length == 0 ? 4 : random.nextInt(expected.length - off);

            ByteSourceFactory sliced = SourceSinkFactories.asSlicedByteSourceFactory(factory, off, len);
            suite.addTest(suiteForBytes(sliced, bytes, name + ".slice[long, long]", desc, false));

            // test a slice() of the ByteSource starting at a random offset with a length of
            // Long.MAX_VALUE
            ByteSourceFactory slicedLongMaxValue =
                    SourceSinkFactories.asSlicedByteSourceFactory(factory, off, Long.MAX_VALUE);
            suite.addTest(
                    suiteForBytes(
                            slicedLongMaxValue, bytes, name + ".slice[long, Long.MAX_VALUE]", desc, false));

            // test a slice() of the ByteSource starting at an offset greater than its size
            ByteSourceFactory slicedOffsetPastEnd =
                    SourceSinkFactories.asSlicedByteSourceFactory(
                            factory, expected.length + 2, expected.length + 10);
            suite.addTest(
                    suiteForBytes(slicedOffsetPastEnd, bytes, name + ".slice[size + 2, long]", desc, false));
        }

        return suite;
    }

    private ByteSource source;

    public ByteSourceTester(
            ByteSourceFactory factory, byte[] bytes, String suiteName, String caseDesc, Method method)
    {
        super(factory, bytes, suiteName, caseDesc, method);
    }

    @Override
    public void setUp() throws IOException
    {
        source = factory.createSource(data);
    }

    public void testOpenStream() throws IOException
    {
        InputStream in = source.openStream();
        try
        {
            byte[] readBytes = ByteStreams.toByteArray(in);
            assertExpectedBytes(readBytes);
        }
        finally
        {
            in.close();
        }
    }

    public void testOpenBufferedStream() throws IOException
    {
        InputStream in = source.openBufferedStream();
        try
        {
            byte[] readBytes = ByteStreams.toByteArray(in);
            assertExpectedBytes(readBytes);
        }
        finally
        {
            in.close();
        }
    }

    public void testRead() throws IOException
    {
        byte[] readBytes = source.read();
        assertExpectedBytes(readBytes);
    }

    public void testCopyTo_outputStream() throws IOException
    {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        source.copyTo(out);
        assertExpectedBytes(out.toByteArray());
    }

    public void testCopyTo_byteSink() throws IOException
    {
        final ByteArrayOutputStream out = new ByteArrayOutputStream();
        // HERESY! but it's ok just for this I guess
        source.copyTo(
                new ByteSink()
                {
                    @Override
                    public OutputStream openStream() throws IOException
                    {
                        return out;
                    }
                });
        assertExpectedBytes(out.toByteArray());
    }

    public void testIsEmpty() throws IOException
    {
        assertEquals(expected.length == 0, source.isEmpty());
    }

    public void testSize() throws IOException
    {
        assertEquals(expected.length, source.size());
    }

    public void testSizeIfKnown() throws IOException
    {
        Optional<Long> sizeIfKnown = source.sizeIfKnown();
        if (sizeIfKnown.isPresent())
        {
            assertEquals(expected.length, (long) sizeIfKnown.get());
        }
    }

    public void testContentEquals() throws IOException
    {
        assertTrue(
                source.contentEquals(
                        new ByteSource()
                        {
                            @Override
                            public InputStream openStream() throws IOException
                            {
                                return new RandomAmountInputStream(
                                        new ByteArrayInputStream(expected), new Random());
                            }
                        }));
    }

    public void testRead_usingByteProcessor() throws IOException
    {
        byte[] readBytes =
                source.read(
                        new ByteProcessor<byte[]>()
                        {
                            final ByteArrayOutputStream out = new ByteArrayOutputStream();

                            @Override
                            public boolean processBytes(byte[] buf, int off, int len) throws IOException
                            {
                                out.write(buf, off, len);
                                return true;
                            }

                            @Override
                            public byte[] getResult()
                            {
                                return out.toByteArray();
                            }
                        });

        assertExpectedBytes(readBytes);
    }

    public void testHash() throws IOException
    {
        HashCode expectedHash = Hashing.md5().hashBytes(expected);
        assertEquals(expectedHash, source.hash(Hashing.md5()));
    }

    public void testSlice_illegalArguments()
    {
        try
        {
            source.slice(-1, 0);
            fail("expected IllegalArgumentException for call to slice with offset -1: " + source);
        }
        catch (IllegalArgumentException expected)
        {
        }

        try
        {
            source.slice(0, -1);
            fail("expected IllegalArgumentException for call to slice with length -1: " + source);
        }
        catch (IllegalArgumentException expected)
        {
        }
    }

    // Test that you can not expand the readable data in a previously sliced ByteSource.
    public void testSlice_constrainedRange() throws IOException
    {
        long size = source.read().length;
        if (size >= 2)
        {
            ByteSource sliced = source.slice(1, size - 2);
            assertEquals(size - 2, sliced.read().length);
            ByteSource resliced = sliced.slice(0, size - 1);
            assertTrue(sliced.contentEquals(resliced));
        }
    }

    private void assertExpectedBytes(byte[] readBytes)
    {
        assertArrayEquals(expected, readBytes);
    }
}
